import collections
import json
import math

import numpy as np

######################################################################################################
#
# Let's say we have a balance time series.
# Given such a sequence, we need to detect outliers in the sequence.
# We know that the total capital quoted in a currency should be a relatively stable number
# (or quasi-static, meaning change slowly).
#
#####################################################################################################
# method 1:
#   get mid price and convert balance vector to total capital (in USDT, for example)
#   if this total capital changes suddenly, then it is an outlier.
#
#####################################################################################################
# method 2:
#  without mid price, we can estimate from balance time series.
#
# Let c be the conversion cofficients (covert the currency value to the quote),
# and we have N balance vectors (row vectors), stack vertically to from matrix M
#  M.c + e (small fluctuation) = ones(N, 1) * y (y is the total capital, a scalar, N is the number of currencies in balance vector)
# Let C = c/y, then M.C + e = ones(N, 1)
#
# C can be estimated using LSQ.
# So for a normal balance vector,  b.C should be quite close to 1. If it is not, then it is an outlier.
#
# We can adapt this algorithm to the more robust RANSAC algorithm, but it is too computationaly expensive.
# For method 2, following is a simple implementation.
########################################################################################################

class GlitchDetector(object):
  def __init__(self, N=10):
    self._N = N
    self._deque = collections.deque(maxlen=N)

    self._coff = None
    self._residual = None

  def has_balance(self, balance):
    for e in self._deque:
      n = np.linalg.norm((np.array(e) - np.array(balance)), np.inf)
      return n < 1e-4

  def check_and_update_balance(self, balance):
    if self._coff is not None:
      err = np.abs(1 - np.dot(balance, self._coff))[0]
      threshold = self._residual * 100

      print(err, self._residual, threshold, err < threshold)
      if err > threshold:
        print('glitch detected!')
        return False

    if self.has_balance(balance):
      return True

    self._deque.append(balance)
    if len(self._deque) < self._N:
      return True

    M = np.vstack(self._deque)
    v = np.ones((self._N, 1))
    try:
      # Find LSQ solution: v = M * x + err
      res = np.linalg.lstsq(M, v, rcond=100)
    except numpy.linalg.linalg.LinAlgError:
      import pdb; pdb.set_trace()

    if res[2] == len(balance):  # rank same as number of currencies
      self._coff = res[0]
      self._residual = math.sqrt(res[1][0])  # residual is a scalar

    return True



############################
# A simple test
############################

# Suppose we have 4 currencies USDT, BTC, ETH, EOS, XRP.
# price: BTC: 10000 USDT
#        ETH: 1000 USDT
#        EOS: 100 USDT
#        XRP: 10 USDT

import random

def to_list(balance_dict):
  s = sorted(balance_dict.items(), key=lambda x: x[0])
  return [x[1] for x in s]


def run_gd():
  curr_price = {
    'USDT': 1,
    'BTC': 10000,
    'ETH': 1000,
    'EOS': 100,
    'XRP': 10,
  }

  curr_balance = {
    'USDT': 100000,
    'BTC': 10,
    'ETH': 100,
    'EOS': 1000,
    'XRP': 10000,
  }

  gd = GlitchDetector(10)

  for nn in range(100):
    print(nn)
    for currency, qty in curr_balance.items():
      if currency == 'USDT':
        continue

      change_qty = random.randint(-1, 1) # random buy/sell qty
      curr_price[currency] *= (1 + np.random.normal(0, 0.001))  # price fluctuates
      print(curr_price)

      if qty < 0:  # sell
        curr_balance['USDT'] -= (change_qty * curr_price[currency])  # more usdt
        curr_balance[currency] += change_qty
      else:  # buy
        usdt_balance = curr_balance['USDT'] - (change_qty * curr_price[currency])
        if usdt_balance > 0:
          curr_balance['USDT'] = usdt_balance
          curr_balance[currency] += change_qty
        else:
          # not enough money
          pass


    new_curr_balance = curr_balance.copy()
    # random glitch
    if nn > 20 and random.randint(1, 20) == 1:
      #new_curr_balance['USDT'] += 10000 # change USDT
      new_curr_balance['BTC'] += 0.1
      #new_curr_balance['ETH'] += 10
      #new_curr_balance['EOS'] += 100
      #new_curr_balance['XRP'] += 1000
      print("glitch")

    balance = to_list(new_curr_balance)

    succ = gd.check_and_update_balance(balance)
    # print(json.dumps(curr_price, indent=2))
    # print(succ)

run_gd()
